/// ===========================================================================
/// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
/// KIND, WHETHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
/// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
/// PURPOSE.
/// ===========================================================================
/// 
/// Project:        MOSS Faceted Search
/// Author:         Leonid Lyublinski (leonidly@microsoft.com)
/// Company:        Microsoft Services
/// Date:           05/04/2007  Version:        1.0
///
/// ===========================================================================

using System;
using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Xml.Serialization;
using System.Data;
using System.Threading;
using Microsoft.SharePoint.WebPartPages;
using Microsoft.Office.Server;
using Microsoft.Office.Server.Search.Administration;
using Microsoft.Office.Server.Search.Query;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using EntLib = Microsoft.Practices.EnterpriseLibrary;
using Microsoft.SharePoint.Portal.ExtendedSearch.Common;

namespace Microsoft.SharePoint.Portal.ExtendedSearch.WebControls
{
    /// <summary>
    /// Custom faceted search web part.
    /// The web part has a series of shared web part properties that can be used
    /// to control the query visually and functionally.
    /// </summary>
    [System.Web.AspNetHostingPermission(System.Security.Permissions.SecurityAction.Demand, Level = System.Web.AspNetHostingPermissionLevel.Minimal)]
    [System.Web.AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = System.Web.AspNetHostingPermissionLevel.Minimal)]
    [ToolboxData("<{0}:SearchFacets runat=server></{0}:SearchFacets>")]
    [XmlRoot(Namespace = "Microsoft.SharePoint.Portal.ExtendedSearch")]
    public class SearchFacets : SearchQueryWebPart, ICallbackEventHandler
    {
        #region Constants
        internal const string key = "FacetedSearch";
        #endregion

        #region Fields
        private short _resultsPerPage;// = 500;
        private string _selectColumns;// = @"<root xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""><FacetColumns><Column Name=""Path"" /></FacetColumns></root>";
        private string _hiddenRule;
        private XmlDocument _xmlColumns;
        private FacetMenu _facets;

        private string _scope;
        private string _defaultScope;
        private string _k;
        private string _sql;
        private int _start;
        private string _v;
        private NameValueCollection _properties;
        private bool _showEmpty;
        private bool _prefixEnabled;
        private bool _showSrcData;
        private bool _cachingEnabled = true;
        private int _longCacheTimeout;
        private int _fastCacheTimeout;

        private string _facetNameCss;// = "ms-navheader";
        private string _facetNameContainerCss;// = "ms-quicklaunch";
        private string _facetValueCss;// = "ms-navitem";
        private string _facetValueContainerCss;// = "ms-navSubMenu2";
        private string _listCss;// = "ms-navsubmenu1";
        private string _listFacetsCss;// = "ms-navsubmenu1";
        private short _cropMax;// = 20;
        private string _facetListId;

        //private Dictionary<string, int> _facetTables;

        private string _facetHtml;
        private bool _run2ndTime;
        private bool _running2ndTime;
        private bool _moreResultsThanFacets;
        private bool _isInPreviewMode;
        private bool _isEditOrDesignMode;
        private bool _usingCachedData;
        private int _processedTotal;
        private DateTime _processedAt;
        private StringCollection _columns;

        private ClientScriptManager csm;
        SearchQuery query;

        private QueryMethodEnum _queryMethod = QueryMethodEnum.KeywordQuery;
        private Microsoft.Office.Server.Search.WebControls.CoreResultsWebPart _coreResultsWebPart;


        #endregion

        #region Properties

        #region Internal
        private StringCollection FacetColumns
        {
            get
            {
                if (_columns == null) _columns = GetFacets();
                return _columns;
            }
        }
        internal FacetMenu Facets
        {
            get { return _facets; }
        }
        #endregion

        #region Cache Settings
        /// <summary>
        /// Enables caching.
        /// </summary>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_CACHE)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_CACHINGENABLED_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_CACHINGENABLED_DESCRIPTION)]
        public bool CachingEnabled
        {
            get
            {
                return _cachingEnabled;
            }
            set
            {
                _cachingEnabled = value;
            }
        }

        /// <summary>
        /// Cache timeout in minutes.
        /// </summary>
        /// <remarks>Using AbsoluteTimeout</remarks>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_CACHE)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_LONGTIMEOUT_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_LONGTIMEOUT_DESCRIPTION)]
        public int LongCacheTimeout
        {
            get
            {
                return _longCacheTimeout;
            }
            set
            {
                if (value < 0) throw new ArgumentOutOfRangeException("LongCacheTimeout", Resource.ERROR_INVALID_TIMEOUT);
                _longCacheTimeout = value;
            }
        }

        /// <summary>
        /// Cache timeout in minutes.
        /// </summary>
        /// <remarks>Using AbsoluteTimeout</remarks>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_CACHE)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_FASTTIMEOUT_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_FASTTIMEOUT_DESCRIPTION)]
        public int FastCacheTimeout
        {
            get
            {
                return _fastCacheTimeout;
            }
            set
            {
                if (value < 0) throw new ArgumentOutOfRangeException("FastCacheTimeout", Resource.ERROR_INVALID_TIMEOUT);
                _fastCacheTimeout = value;
            }
        }
        #endregion

        #region Data Settings
        /// <summary>
        /// Number of results returned by the search engine in one resultset.
        /// The higher value, the more accurate is the calculation of facet hits.
        /// </summary>
        /// <remarks>High value will result in performance penalty.</remarks>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_DATA)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_SELECTCOLUMNS_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_SELECTCOLUMNS_DESCRIPTION)]
        public short ResultsPerPage
        {
            get
            {
                return _resultsPerPage;
            }
            set
            {
                _resultsPerPage = value;
            }
        }

        /// <summary>
        /// Selected columns are defined as managed properties and will be available as search facets. 
        /// The columns must be crawled and set as managed properties in the SSP administration.
        /// </summary>
        /// <remarks>Test against existing managed properties is built-in, however a proper exception propagation is still required.</remarks>
        /// <permission cref=""></permission>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_DATA)]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_SELECTCOLUMNS_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_SELECTCOLUMNS_DESCRIPTION)]
        public string SelectColumns
        {
            get
            {
                return _selectColumns;
            }
            set
            {
                _selectColumns = value;
            }
        }

        [WebBrowsable(false)]
        [Category(Constants.WP_CATEGORYNAME_DATA)]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_HIDDENRULE_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_HIDDENRULE_DESCRIPTION)]
        public string HiddenRule
        {
            get { return _hiddenRule; }
            set { _hiddenRule = value; }
        }

        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_DATA)]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_SCOPE_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_SCOPE_DESCRIPTION)]
        public string DefaultScope
        {
            get { return _defaultScope; }
            set { _defaultScope = value; }
        }
        

        /// <summary>
        /// Adds prefix search to the Facet match.
        /// </summary>
        /// <remarks>Hide the propertye</remarks>
        [WebBrowsable(false)]
        [Category(Constants.WP_CATEGORYNAME_DATA)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_PREFIXENABLED_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_PREFIXENABLED_DESCRIPTION)]
        public bool PrefixEnabled
        {
            get { return _prefixEnabled; }
            set { _prefixEnabled = value; }
        }

        /// <summary>
        /// Choice of Query method between FullText and Keyword
        /// </summary>
        [WebBrowsable(false)]
        [Category(Constants.WP_CATEGORYNAME_DATA)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_QUERYMETHOD_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_QUERYMETHOD_DESCRIPTION)]
        public QueryMethodEnum QueryMethod
        {
            get { return _queryMethod; }
            set { _queryMethod = value; }
        }
        
        #endregion

        #region CSS Classes
        /// <summary>
        /// The CSS class for the list of facets.
        /// </summary>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_CSS)]
        [PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_LISTCSS_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_LISTCSS_DESCRIPTION)]
        public string ListCss
        {
            get
            {
                return _listCss;
            }
            set
            {
                _listCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face name.
        /// </summary>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_CSS)]
        [PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_FACETNAMECONTAINERCSS_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_FACETNAMECONTAINERCSS_DESCRIPTION)]
        public string FacetNameContainerCss
        {
            get
            {
                return _facetNameContainerCss;
            }
            set
            {
                _facetNameContainerCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face name.
        /// </summary>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_CSS)]
        [PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_FACETNAMECSS_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_FACETNAMECSS_DESCRIPTION)]
        public string FacetNameCss
        {
            get
            {
                return _facetNameCss;
            }
            set
            {
                _facetNameCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the list of facets.
        /// </summary>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_CSS)]
        [PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_FACETLISTCSS_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_FACETLISTCSS_DESCRIPTION)]
        public string FacetsListCss
        {
            get
            {
                return _listFacetsCss;
            }
            set
            {
                _listFacetsCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face value.
        /// </summary>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_CSS)]
        [PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_FACETVALUECONTAINERCSS_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_FACETVALUECONTAINERCSS_DESCRIPTION)]
        public string FacetValueContainerCss
        {
            get
            {
                return _facetValueContainerCss;
            }
            set
            {
                _facetValueContainerCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face value and # of hits.
        /// </summary>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_CSS)]
        [PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_FACETVALUECSS_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_FACETVALUECSS_DESCRIPTION)]
        public string FacetValueCss
        {
            get
            {
                return _facetValueCss;
            }
            set
            {
                _facetValueCss = value;
            }
        }
        #endregion

        #region Visual Behaviour
        /// <summary>
        /// Maximum number of characters in a facet before truncation. After the facet value reached the maximum, 
        /// it is truncated, "..." aded to the end. Also, ToolTip is added for long values with a full text.
        /// </summary>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_VISUAL)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_CROPMAX_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_CROPMAX_DESCRIPTION)]
        public short CropMax
        {
            get
            {
                return _cropMax;
            }
            set
            {
                _cropMax = value;
            }
        }

        /// <summary>
        /// Will cause show facets that don't have values.
        /// </summary>
        [WebBrowsable(true)]
        [Category(Constants.WP_CATEGORYNAME_VISUAL)]
        [PersistenceMode(PersistenceMode.Attribute)]
        [Personalizable(PersonalizationScope.Shared)]
        [WebDisplayName(Constants.WP_SEARCHFACETS_SHOWEMPTYFACET_FRIENDLYNAME)]
        [WebDescription(Constants.WP_SEARCHFACETS_SHOWEMPTYFACET_DESCRIPTION)]
        public bool ShowEmptyFacet
        {
            get { return _showEmpty; }
            set { _showEmpty = value; }
        }
        #endregion

        #endregion

        #region Delegates
        /// <summary>
        /// The method fires for each bound DataList item and dynamically builds its content.
        /// Building of the content is done via population of Controls collection.
        /// </summary>
        /// <param name="sender">DataList used as a placeholder for facets.</param>
        /// <param name="e"><typeparamref name="DataListItemEventArgs"/></param>
        /// <remarks>Each DataList item represents one Facet, rendered as a label and a set of Facet Values, rendered as a datalist.</remarks>
        protected void listFacets_ItemDataBound(object sender, DataListItemEventArgs e)
        {
            DataListItem item = e.Item;
            DataList list = new DataList();
            list.CssClass = _listFacetsCss;

            FacetCollection facetCollection = (FacetCollection)item.DataItem;
            list.ID = facetCollection.Name;
            list.Attributes.Add("facet", facetCollection.Name);
            list.Attributes.Add("count", facetCollection.Count.ToString());
            list.Attributes.Add("totalresults", GetColumnAttribute(facetCollection.Name, "TotalResults"));
            list.ItemCommand += new DataListCommandEventHandler(list_ItemCommand);
            list.ShowFooter = false;
            list.ShowHeader = true;

            // facet name
            string facetCollectionName = (GetColumnAttribute(facetCollection.Name, "DisplayName") == null) ? facetCollection.Name : GetColumnAttribute(facetCollection.Name, "DisplayName");
            //list.HeaderTemplate = new HeaderTemplate(ListItemType.Header, _facetNameCss, facetCollection.Name, facetCollectionName, string.Format("total facets:{0}, total hits:{1}, top hit:{2}", facetCollection.Count, facetCollection.Hits, facetCollection.Max), -1, facetCollection.Count, _run2ndTime && _moreResultsThanFacets && !_running2ndTime);  // move progress to the menu header
            list.HeaderTemplate = new HeaderTemplate(ListItemType.Header, _facetNameCss, facetCollection.Name, facetCollectionName, string.Format("total facets:{0}, total hits:{1}, top hit:{2}", facetCollection.Count, facetCollection.Hits, facetCollection.Max), -1, facetCollection.Count, false);
            list.HeaderStyle.Width = Unit.Percentage(100);
            list.HeaderStyle.CssClass = _facetNameContainerCss;

            // facet value
            list.ItemTemplate = new FacetLightTemplate(ListItemType.Item, Query, _facetValueCss, _cropMax, facetCollection.Name);
            //if (_run2ndTime && _running2ndTime)
            //    list.ItemTemplate = new FacetLightTemplate(ListItemType.Item, _facetValueCss, _cropMax, facetCollection.Name);
            //else
            //    list.ItemTemplate = new FacetTemplate(ListItemType.Item, _facetValueCss, _cropMax);
            list.ItemStyle.Width = Unit.Percentage(100);
            list.ItemStyle.CssClass = _facetValueContainerCss;


            // list.CssClass = _listCss;
            list.Width = Unit.Percentage(100);
            list.DataSource = facetCollection;

            // adding sorting
            string orderBy = GetColumnAttribute(facetCollection.Name, "OrderBy");
            if (!string.IsNullOrEmpty(orderBy))
            {
                try
                {
                    FacetSortEnum facetOrderBy = FacetSortEnum.None;
                    EnumConverter converter = (EnumConverter)TypeDescriptor.GetConverter(facetOrderBy);
                    facetOrderBy = (FacetSortEnum)converter.ConvertFromString(orderBy);
                    facetCollection.SortOrder = facetOrderBy;
                    facetCollection.Sort(facetCollection);
                }
                catch
                {
                    throw new NotSupportedException(Resource.ERROR_CONVERTING_ORDERBY);
                }
            }

            // adding paging
            string maxResults = GetColumnAttribute(facetCollection.Name, "TotalResults");
            if (!string.IsNullOrEmpty(maxResults))
            {
                int totalResults;
                if (int.TryParse(maxResults, out totalResults))
                {
                    if (totalResults > -1 && facetCollection.Count > totalResults) // means all results
                    {
                        //list.HeaderTemplate = new HeaderTemplate(ListItemType.Header, _facetNameCss, facetCollection.Name, facetCollectionName, string.Format("total facets:{0}, total hits:{1}, top hit:{2}", facetCollection.Count, facetCollection.Hits, facetCollection.Max), totalResults, facetCollection.Count, _run2ndTime && _moreResultsThanFacets && !_running2ndTime);// move progress to the menu header
                        list.HeaderTemplate = new HeaderTemplate(ListItemType.Header, _facetNameCss, facetCollection.Name, facetCollectionName, string.Format("total facets:{0}, total hits:{1}, top hit:{2}", facetCollection.Count, facetCollection.Hits, facetCollection.Max), totalResults, facetCollection.Count, false);

                        // review styles if new web part properties needed
                        list.FooterTemplate = new FooterTemplate(ListItemType.Footer, _facetValueCss, list.ID, totalResults);
                        list.FooterStyle.Width = Unit.Percentage(100);
                        list.FooterStyle.CssClass = _facetValueContainerCss;
                        list.ShowFooter = true;

                        //_facetTables.Add(list.ID, totalResults);
                    }
                }
            }

            list.DataBind();
            item.Controls.Add(list);
        }

        protected void list_ItemDataBound(object sender, DataListItemEventArgs e)
        {
            DataListItem item = e.Item;
            int i = item.ItemIndex;
            DataList list = (DataList)sender;
            string id = list.ID;
            string totalResults = GetColumnAttribute(list.ID, "TotalResults");
            if (!string.IsNullOrEmpty(totalResults))
            {
                int totalResultsInt = int.Parse(totalResults);
                if (i > totalResultsInt)
                    item.Attributes.Add("visible", "true");
                else
                    item.Attributes.Add("visible", "false");
            }

        }

        /// <summary>
        /// The method fires when the Facet link clicked in the facet menu. Functionality is repsonsible for refining the search results.
        /// The code parses Facet and its value, reconstructs the querystring and redirects the user to a constructed query.
        /// </summary>
        /// <param name="source">DataList</param>
        /// <param name="e"><typeparamref name="DataListCommandEventArgs"/></param>
        /// <remarks>Currently implemented as a postback. Consider redoing as apure hyperlink instead.</remarks>
        protected void list_ItemCommand(object source, DataListCommandEventArgs e)
        {
            if (e.CommandName == "SelectFacet")
            {
                DataList list = (DataList)source;
                string facetName = list.ID;
                LinkButton link = (LinkButton)e.CommandSource;
                // account for truncation
                string facetValue = (string.IsNullOrEmpty(link.ToolTip)) ? link.Text : link.ToolTip;
                int hits = int.Parse(link.CommandArgument);
                if (_properties[facetName] == null)
                    _properties.Set(facetName, facetValue);
                else
                    _properties[facetName] = facetValue;
                //this is the part that should be changed if support of multiple facet values per facet to be supported
                // will require revise of SearchQuery too

                RefineSearch();
            }
            //throw new Exception("The method or operation is not implemented.");
        }

        /// <summary>
        /// Constructs the Controls collection of the webpart.
        /// </summary>
        /// <remarks>Consider test against Page.IsPostBack event along with stateful controls. Currently is not implemented</remarks>
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            try
            {
                if (!Query.IsEmpty())
                {
                    query = Query;
                    _k = query.Keywords;
                    // the Scope will be transparent to its source - Url or WP property.
                    _scope = (string.IsNullOrEmpty(query.Scope))? _defaultScope: query.Scope;
                    _start = query.Start;
                    _v = query.View;
                    _properties = query.Properties;
                    //StringCollection columns = GetFacets();
                    if (!string.IsNullOrEmpty(_hiddenRule))
                    {
                        string key = _hiddenRule.Substring(0, _hiddenRule.IndexOf('=')).Trim();
                        string value = _hiddenRule.Substring(_hiddenRule.IndexOf('=') + 1).Trim();
                        _properties.Add(key, value);
                    }                                     
                } 
                else if (Parent != null && Parent.GetType().FullName == "Microsoft.SharePoint.WebPartPages.WebPartPreview")
                {
                    // fake something for WP preview only
                    query = new SearchQuery();
                    query.Keywords = _k = "MSDN";
                } 
                else if (_isEditOrDesignMode)
                {// fake something for WP preview only
                    query = new SearchQuery();
                    query.Keywords = _k = "MSDN";
                }
                else if (Query.Start>0)
                {
                    // paging in advanced search
                    // consider extracting SearchQuery from e.g. ViewState
                }

                DataList listFacets = CreateFacetsList();
                Controls.Add(HeaderTable(_processedTotal));
                if (listFacets != null)
                {
                    _facetListId = listFacets.ClientID;
                    Controls.Add(listFacets);
                }

            }
            catch (Exception ex)
            {
                Utility.HandleException(ex, true, Resource.ErrorMessageSearchFacets, Controls);
            }
        }

        protected override void OnInit(EventArgs e)
        {            
            base.OnInit(e);
            csm = Page.ClientScript;
            if (this.Parent != null)
                _isInPreviewMode = Parent.GetType().FullName == "Microsoft.SharePoint.WebPartPages.WebPartPreview";
            if (!_isInPreviewMode)
            {
                if (this.WebPartManager.DisplayMode == WebPartManager.EditDisplayMode ||
                    this.WebPartManager.DisplayMode == WebPartManager.DesignDisplayMode)
                    _isEditOrDesignMode = true;
            }
            _run2ndTime = ResultsPerPage > Constants.RESULTS_PER_PAGE_MAX && !_isEditOrDesignMode;
            ValidateColumns();
            InitClientScripts();
        }

        #endregion

        #region Private Methods
        /// <summary>
        /// Builds a search string and redirects request using a built querystring.
        /// </summary>
        private void RefineSearch()
        {
            SearchQuery query = Query;
            query.Keywords = _k;
            query.Scope = _scope;
            query.Start = _start;
            query.View = _v;
            query.Properties = _properties;

            string qs = Utility.BuildQueryString(query);
            string qsEncoded = string.Format("{0}?{1}", Page.Request.Url.AbsolutePath, qs);
            Page.Response.Redirect(qsEncoded); // consider use of HyperLinks w/o posts
        }

        /// <summary>
        /// Processes received search results and build a facet structure with totals.
        /// </summary>
        /// <param name="dt"></param>
        private void BuildFacets(DataTable dt)
        {
            // our SELECT statement already lists all facets as columns.
            // no need to check, just skip column #1 Path
            //StringCollection facets = GetFacets();
            FacetMenu facetMenu = new FacetMenu();

            Regex re = new Regex(Constants.REGEX_LOOKUP_SPLITTER, RegexOptions.Singleline);

            foreach (DataRow row in dt.Rows)
            { // process each result
                for (int i = 0; i < dt.Columns.Count; i++)
                {
                    if (dt.Columns[i].ColumnName.ToLower() == "path") continue;// process each meta column except Path
                    string facetName = dt.Columns[i].ColumnName.Trim();
                    if (row[facetName] is string[]) // found multiple values!!!
                    {
                        string[] facetValues = (string[])row[facetName];
                        foreach (string facetValue in facetValues)
                        {
                            //probably a bug. The crawled property contains ;#5; separated string
                            AddMultipleFacets(facetMenu, facetName, facetValue);
                        }
                    }
                    else
                    {
                        // prepare for lookup column with multiple selection
                        string facetValue = row[i].ToString().Trim();
                        AddMultipleFacets(facetMenu, facetName, facetValue);
                    }
                }
            }
            //remove empty facets so they don't show up in the facet menu
            _facets = facetMenu;
        }

        private HtmlTable HeaderTable(int processedTotal)
        {
            HtmlTable table = new HtmlTable();
            table.CellSpacing = table.CellPadding = 0;
            table.Border = 0;
            //table.Width = "140px";
            
            HtmlTableRow rowProgress = new HtmlTableRow();
            rowProgress.Height = "20px";
            HtmlTableCell cellProgress = new HtmlTableCell();
            cellProgress.Attributes.Add("class", "ms-sctabrx");
            cellProgress.VAlign = "middle";
            cellProgress.NoWrap = true;
            rowProgress.Cells.Add(cellProgress);

            HtmlImage img = new HtmlImage();
            img.Src = "/_layouts/images/CRPERSPC.gif";
            img.Border = 0;
            img.ID = "ProgressBar";
            img.Alt = string.Format(Resource.SEARCH_FACETS_FACETCOUNTER, processedTotal);
            cellProgress.Controls.Add(img);

            HtmlGenericControl resultCount = new HtmlGenericControl("span");
            resultCount.InnerText = string.Format(Resource.SEARCH_FACETS_FACETCOUNTER, processedTotal);
            resultCount.ID = "ResultCount";
            cellProgress.Controls.Add(resultCount);
            
            if (_run2ndTime && _moreResultsThanFacets)
            {
                resultCount.Style.Add("display", "none");
                img.Style.Add("display", "inline");
            }
            else
            {
                resultCount.Style.Add("display", "inline");
                img.Style.Add("display", "none");
            }
            
            HtmlTableCell cellRefreshLink = new HtmlTableCell();
            cellRefreshLink.Align = "right";
            cellRefreshLink.Attributes.Add("class", "ms-sctabrx");
            cellRefreshLink.NoWrap = true;
            cellRefreshLink.VAlign = "middle";
            rowProgress.Cells.Add(cellRefreshLink);
            
            LinkButton releaseCache = new LinkButton();
            releaseCache.ID = "Refresh";
            releaseCache.Click += new EventHandler(ReleaseCache_Click);
            //releaseCache.OnClientClick = "setFacetsHeader(" + _processedTotal.ToString() + ", false);";
            releaseCache.Text = Resource.SEARCH_FACETS_REFRESH;
            cellRefreshLink.Controls.Add(new LiteralControl("&nbsp;|&nbsp;"));
            cellRefreshLink.Controls.Add(releaseCache);
            
            HtmlGenericControl caching = new HtmlGenericControl("span");
            caching.ID = "Caching";
            caching.InnerText = Resource.SEARCH_FACETS_NOTCACHED;
            cellRefreshLink.Controls.Add(caching);
            
            HtmlTableCell cellRefresh = new HtmlTableCell();
            cellRefresh.Align = "right";
            cellRefresh.Attributes.Add("class", "ms-main");
            cellRefresh.NoWrap = true;
            cellRefresh.VAlign = "middle";
            rowProgress.Cells.Add(cellRefresh);

            HtmlImage emptyImg = new HtmlImage();
            emptyImg.Src = "/_layouts/images/empty.gif";
            emptyImg.Attributes.Add("hspace", "3");
            cellRefresh.Controls.Add(emptyImg);
            
            HtmlImage releaseCacheImg = new HtmlImage();
            releaseCacheImg.ID = "RefreshIcon";
            releaseCacheImg.Src = "/_layouts/images/recur.gif";
            releaseCacheImg.Alt = Resource.SEARCH_FACETS_REFRESH;
            cellRefresh.Controls.Add(releaseCacheImg);

            HtmlImage noCacheImg = new HtmlImage();
            noCacheImg.ID = "NoCacheIcon";
            noCacheImg.Src = "/_layouts/images/recurex.gif";
            noCacheImg.Alt = Resource.SEARCH_FACETS_NOTCACHED;
            cellRefresh.Controls.Add(noCacheImg);

            if (_usingCachedData)
            {
                releaseCache.Style.Add("display", "inline");
                releaseCache.ToolTip = Resource.SEARCH_FACETS_REFRESH_ALT;
                noCacheImg.Style.Add("display", "none");
                caching.Style.Add("display", "none");
                releaseCacheImg.Style.Add("display", "inline");
                releaseCacheImg.Alt = string.Format(Resource.SEARCH_FACETS_REFRESH_IMG_ALT, _processedAt);
            }
            else
            {
                releaseCache.Style.Add("display", "none");
                noCacheImg.Style.Add("display", "inline");
                caching.Style.Add("display", "inline");
                releaseCacheImg.Style.Add("display", "none");
            }

            table.Rows.Add(rowProgress);

            HtmlTableRow rowHeader = new HtmlTableRow();
            HtmlTableCell cellHeader = new HtmlTableCell();
            cellHeader.ColSpan = 3;
            cellHeader.Height = "5px";
            cellHeader.Attributes.Add("class", "ms-main");
            cellHeader.NoWrap = true;
            rowHeader.Cells.Add(cellHeader);

            cellHeader.Controls.Add(emptyImg);

            table.Rows.Add(rowHeader);


            return table;
        }

        void ReleaseCache_Click(object sender, EventArgs e)
        {
            SearchProcessor searchProcessor = new SearchProcessor(_cachingEnabled, this.Page, FacetColumns, query, _prefixEnabled, _moreResultsThanFacets, _run2ndTime, _running2ndTime);
            searchProcessor.ReleaseCache(out _usingCachedData);
        }


        /// <summary>
        /// Helper method to populate facet menu
        /// </summary>
        /// <param name="facetMenu"></param>
        /// <param name="facetName"></param>
        /// <param name="facetValue"></param>
        private void AddFacet(FacetMenu facetMenu, string facetName, string facetValue)
        {
            if (String.IsNullOrEmpty(facetName.Trim())) return;
            if (String.IsNullOrEmpty(facetValue.Trim()))
            {
                if (_showEmpty)
                    facetValue = Constants.UNNAMED_FACET_VALUE;
                else
                    return;
            }

            if (!facetMenu.ContainsFacetCollection(facetName)) facetMenu.Add(new FacetCollection(facetName.Trim()));

            FacetCollection facetCounter = facetMenu.GetFacetCollection(facetName.Trim());
            Facet facet = facetCounter.GetFacet(facetValue.Trim());
            if (facet == null)
                facetCounter.Add(new Facet(facetValue.Trim(), 1));
            else
                facet.Hits += 1;
        }

        /// <summary>
        /// Helper method to populate facet menu with facets from lookup fields
        /// </summary>
        /// <param name="facetMenu"></param>
        /// <param name="facetName"></param>
        /// <param name="facetValue"></param>

        private void AddMultipleFacets(FacetMenu facetMenu, string facetName, string facetValue)
        {
            string[] facetValues = Splitter.Split(facetValue);
            if (facetValues != null)
            {
                //probably a bug. The crawled property contains ;#5; separated string
                foreach (string lookup in facetValues)
                {
                    if (lookup != null)
                        AddFacet(facetMenu, facetName, lookup.Trim());
                }
            }
            else
            {
                AddFacet(facetMenu, facetName, facetValue.Trim());
            }
        }


        private StringCollection GetFacets()
        {

            StringCollection selectedFacetsCollection = new StringCollection();

            //Get selected columns
            _xmlColumns = new XmlDocument();
            try
            {
                if (!string.IsNullOrEmpty(SelectColumns))
                {
                    _xmlColumns.LoadXml(SelectColumns);
                    XmlNodeList selectedFacets = _xmlColumns.GetElementsByTagName("Column");
                    //Populate column name collection
                    for (int i = 0; i < selectedFacets.Count; i++)
                    {
                        XmlElement column = (XmlElement)selectedFacets[i];
                        string name = column.GetAttribute("Name");
                        selectedFacetsCollection.Add(name);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new ApplicationException(Resource.ERROR_LOADING_XML, ex);
            }
            return selectedFacetsCollection;
        }

        /// <summary>
        /// Returns value of the Column attribute in XML string set by SelectColumns property
        /// </summary>
        /// <param name="attName">name of the attribute</param>
        /// <param name="columnName">name of the column</param>
        /// <returns></returns>

        #region FacetMenu Refinement
        private string GetColumnAttribute(string columnName, string attName)
        {
            try
            {
                // building smth like \\Column[@Name='Product']@OrderBy
                string xpath = string.Format("//Column[@Name='{0}']/@{1}", columnName, attName);
                XmlNode node = _xmlColumns.SelectSingleNode(xpath);
                if (node == null) return null;
                else return node.Value;
            }
            catch (XPathException ex)
            {
                return null;
            }
        }

        private bool HasExclusion(string columnName)
        {
            try
            {
                string xpath = string.Format("//Column[@Name='{0}']", columnName);
                XmlNode column = _xmlColumns.SelectSingleNode(xpath);
                if (!column.HasChildNodes) return false;
                return column.SelectNodes("Exclusions") != null;
            }
            catch (Exception ex)
            {
                return false;
            }
        }
        private bool Excluded(string columnName, string facetName)
        {
            try
            {
                //string xpath = string.Format("//Column[@Name='{0}']/Exclusions/Exclusion/@Match", columnName);
                string xpath = string.Format("//Column[@Name='{0}']/Exclusions/Exclusion[(contains(@Match, '%') and starts-with('{1}', substring-before(@Match, '%'))) or (@Match = '{1}')]/@{2}", columnName, facetName, "Match");
                // add loop thru exclusions
                XmlNode node = _xmlColumns.SelectSingleNode(xpath);
                if (node != null)
                {
                    string pattern = node.Value;
                    return Utility.IsMatch(pattern, facetName);
                }
                return false;
            }
            catch
            {
                return false;
            }
        }
        private void ApplyExclusions()
        {
            for (int j = 0; j < _facets.Count; j++)
            {
                FacetCollection col = _facets[j];
                if (!HasExclusion(col.Name)) continue;
                for (int i = 0; i < col.Count; i++)
                {
                    Facet facet = col[i];
                    if (Excluded(col.Name, facet.Name))
                    {
                        col.Remove(facet);
                        i--;
                    }
                }
                if (col.Count == 0) _facets.Remove(col); // removing empty facet collections
            }
        }

        private void ApplyRelationships()
        {
            for (int j = 0; j < _facets.Count; j++)
            {
                FacetCollection col = _facets[j];
                if (!HasParent(col.Name)) // column doesn't have a parent. easy
                {
                    ApplyFacetRelationships(col);
                }
                else
                {
                    if (!_properties.HasKeys()) // no parent - no check neeeded
                    {
                        _facets.Remove(col);
                        j--;
                    }
                    else
                    {
                        string parentName = GetColumnAttribute(col.Name, "ParentName");
                        string parentValue = GetColumnAttribute(col.Name, "ParentValue");
                        if (!string.IsNullOrEmpty(_properties[parentName])) // have hit on name
                        {
                            if (!string.IsNullOrEmpty(parentValue))
                            {
                                if (_properties[parentName].Equals(parentValue, StringComparison.CurrentCultureIgnoreCase)) // have hit on value
                                {
                                    // do nothing at the column level - relationship is in place
                                    ApplyFacetRelationships(col);
                                }
                                else
                                {
                                    _facets.Remove(col);
                                    j--;
                                }
                            }
                            else
                            {
                                // don't have to do anything else at the column level. we passed
                                ApplyFacetRelationships(col);
                            }
                        }
                        else
                        {
                            _facets.Remove(col);
                            j--;
                        }
                    }
                }
            }
        }

        private void ApplyFacetRelationships(FacetCollection col)
        {
            for (int i = 0; i < col.Count; i++)
            {
                Facet facet = col[i];
                XmlNode mapping = GetMapping(col.Name, facet.Name);
                if (mapping == null) continue;
                if (mapping.Attributes["ParentName"] == null)
                    continue; // done here
                string parentName = mapping.Attributes["ParentName"].Value;
                if (string.IsNullOrEmpty(_properties[parentName]))  // no defined column in the URL
                {
                    col.Remove(facet);
                    i--;
                }
                else
                {
                    string parentValue = mapping.Attributes["ParentValue"].Value;
                    if (_properties[parentName].Equals(parentValue, StringComparison.CurrentCultureIgnoreCase)) // have hit on value
                    {
                        // do nothing at the facet level - relationship is in place
                        continue;
                    }
                    else
                    {
                        _facets.Remove(col);
                        i--;
                    }
                }
            }
        }

        private bool HasMapping(string columnName)
        {
            try
            {
                string xpath = string.Format("//Column[@Name='{0}']", columnName);
                XmlNode column = _xmlColumns.SelectSingleNode(xpath);
                if (column == null) return false;
                if (!column.HasChildNodes) return false;
                return column.SelectNodes("Mappings") != null;
            }
            catch (XPathException ex)
            {
                return false;
            }
        }

        /// <summary>
        /// Retruns Mapping as XmlNode
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="facetName">Facet name (@Name attribute)</param>
        /// <param name="attName">Atribute name</param>
        /// <returns></returns>
        private XmlNode GetMapping(string columnName, string facetName)
        {
            try
            {
                //string xpath = string.Format("//Column[@Name='{0}']/Mappings/Mapping[starts-with('{1}',substring-before(@Match, '%'))]/@{2}", columnName, facetName, attName);
                /* the following line define XPATH expression. The goal is to provide different processing for
                 * wild-card Matches (where Match parameter ends with '%', but I check contains as there is no ends-with function in XSL1.0)
                 * and regular parameter that implies explicit equal                 
                 * */
                string xpath = string.Format("//Column[@Name='{0}']/Mappings/Mapping[(contains(@Match, '%') and starts-with('{1}', substring-before(@Match, '%'))) or (@Match = '{1}')]", columnName, facetName);

                XmlNode node = _xmlColumns.SelectSingleNode(xpath);

                return node;
            }
            catch
            {
                return null;
            }
        }
        /// <summary>
        /// Retruns value of the Column attribute
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="facetName">Facet name (@Name attribute)</param>
        /// <param name="attName">Atribute name</param>
        /// <returns></returns>
        private string GetMappingAttribute(string columnName, string facetName, string attName)
        {
            XmlNode node = GetMapping(columnName, facetName);

            if (node == null) return null;
            if (node.Attributes[attName] == null) return null;
            return node.Attributes[attName].Value;
        }

        private void ApplyMappings()
        {
            for (int j = 0; j < _facets.Count; j++)
            {
                FacetCollection col = _facets[j];
                FacetCollection clone = (FacetCollection)col.Clone();
                if (!HasMapping(col.Name)) continue;
                string facetDefaultImage = GetColumnAttribute(col.Name, "DefaultImage");
                for (int i = 0; i < col.Count; i++)
                {
                    Facet facet = col[i];
                    if (!string.IsNullOrEmpty(facetDefaultImage)) facet.Image = facetDefaultImage;

                    string match = GetMappingAttribute(col.Name, facet.Name, "Match");
                    if (!string.IsNullOrEmpty(match))
                    {
                        string displayName = GetMappingAttribute(col.Name, facet.Name, "DisplayName");
                        if (!string.IsNullOrEmpty(displayName)) facet.DisplayName = displayName;

                        string facetImage = GetMappingAttribute(col.Name, facet.Name, "Image");
                        if (!string.IsNullOrEmpty(facetImage)) facet.Image = facetImage;

                        //if (match.EndsWith("%"))
                        //{
                        //    string prefix = match.TrimEnd('%');
                        //    foreach (Facet f in clone) {
                        //        if (f.Name.StartsWith(prefix) && (f.Name != facet.Name)) // consider use Culture based comparison
                        //            facet.Hits += f.Hits;
                        //    }
                        //}
                    }
                }
            }
        }

        private bool HasParent(string columnName)
        {
            string xpath = string.Format("//Column[@Name='{0}']", columnName);
            XmlNode node = _xmlColumns.SelectSingleNode(xpath);
            if (node == null) return false;
            if (node.Attributes["ParentName"] == null) return false;
            else
            {
                string parentName = node.Attributes["ParentName"].Value;
                //StringCollection facets = GetFacets();
                if (!FacetColumns.Contains(parentName)) return false;
            }
            return true;
        }

        /// <summary>
        /// Returns value of the FacetColumns attribute in XML string set by SelectColumns property
        /// </summary>
        /// <param name="attName">name of the attribute</param>
        /// <returns></returns>
        private string GetColumnsAttribute(string attName)
        {
            try
            {
                XmlNode columns = _xmlColumns.DocumentElement.ChildNodes[0];
                if (columns.Attributes[attName] != null) return columns.Attributes[attName].Value;
                return null;
            }
            catch (XPathException ex)
            {
                return null;
            }
        }
        #endregion



        private void InitClientScripts()
        {
            if (!csm.IsClientScriptIncludeRegistered(key))
            {
                //+++++++ LEAVE FOR FUTURE REFACTORING TO EMBEDDED RESOURCES +++++++//
                //string scriptName = Constants.SCRIPT_NAME;
                //System.Reflection.ManifestResourceInfo mri = this.GetType().Assembly.GetManifestResourceInfo(scriptName);
                //Debug.Assert(null != mri);
                //string scriptUrl = csm.GetWebResourceUrl(this.GetType(), scriptName);
                //csm.RegisterClientScriptResource(this.GetType(), scriptName);
                string scriptUrl = "/_layouts/FacetedSearch.js";
                csm.RegisterClientScriptInclude(this.GetType(), key, scriptUrl);
            }
            if (!csm.IsClientScriptBlockRegistered(key))
            {
                #region Common scripts
                Control ctrl = FindControl("Facets");
                Control progressBar = FindControl("ProgressBar");
                Control refreshIcon = FindControl("RefreshIcon");
                Control noCacheIcon = FindControl("NoCacheIcon");
                Control resultCount = FindControl("ResultCount");
                Control caching = FindControl("Caching");
                Control refresh = FindControl("Refresh");

                if (!csm.IsStartupScriptRegistered(this.GetType(), key + "_ProcessFacetTables"))
                {
                    string commonScript = (ctrl != null) ? @"
function processServerData(arg, context) {
    var facets = document.getElementById('" + ctrl.ClientID + @"');
    var args = arg.split('" + Constants.HTML_DELIMITER + @"');
    var facetHTML = args[0];
    var processedTotal = parseInt(args[1]);
    var usingCachedData = eval(args[2]);
    var processedAt = args[3];
    if (facets) {
        facets.outerHTML = facetHTML;
        processFacetTables();
    }
    setFacetsHeader(processedTotal, usingCachedData, processedAt);
}                  
processFacetTables();" : "processFacetTables();";
                    
                    csm.RegisterStartupScript(this.GetType(), key + "_ProcessFacetTables", commonScript, true);
                }
                #endregion

                #region 2nd run scripts
                if (_run2ndTime)
                {
                    if (ctrl == null) return;
                    if (!csm.IsClientScriptBlockRegistered(this.GetType(), key + "_CallBack"))
                    {
                        string cbReference = csm.GetCallbackEventReference(this, "arg", "processServerData", "context", _processedTotal.ToString(), false);
                        string callBack = @"
function callServer(arg, context) {
    try {" +
           cbReference +
@";
    } catch(e) {
        alert(e.message);
    }
}
                        
function setFacetsHeader(procTotal, usingCachedData, procAt) {
    switchElement('" + progressBar.ClientID + @"', false);   
    switchElement('" + resultCount.ClientID + @"', true);   
    setResultCount('" + resultCount.ClientID + @"', procTotal)
    switchElement('" + refreshIcon.ClientID + @"', usingCachedData); 
    switchElement('" + noCacheIcon.ClientID + @"', !usingCachedData);
    switchElement('" + refresh.ClientID + @"', usingCachedData); 
    switchElement('" + caching.ClientID + @"', !usingCachedData); 
    if (procAt)
        setElAttribute('" + refresh.ClientID + @"', 'Alt', procAt);
}

function setElAttribute(id, attName, attValue){
    var el=document.getElementById(id);
	el[attName] = attValue;
}

function setResultCount(id, procTotal){
    var el=document.getElementById(id);
	if (el !=null) {
        el.innerText = '" + Resource.SEARCH_FACETS_FACETCOUNTER + @"'.replace('{0}', procTotal);
    }	
}
           
                        ";
                        csm.RegisterClientScriptBlock(this.GetType(), key + "_CallBack", callBack, true);
                    }

                    if (!csm.IsStartupScriptRegistered(this.GetType(), key + "_CallServer"))
                    {
                        string xml = query.GetXml().InnerXml;// for advanced search
                        csm.RegisterStartupScript(this.GetType(), key + "_CallServer", "if (refineFacets) callServer('" + xml + "', " + _processedTotal.ToString() + ");", true);
                    }
                }
                #endregion
            }


        }

        private DataList CreateFacetsList()
        {
            /* The idea here is not to render a datatable w/ data if we will have a 2nd run anyways.
             * So how to find out if there will be a 2nd run?
             * The 2nd run will happen if _run2ndTime && _moreResultsThanFacets
             * IsPostBack indicates click on the facet if FacetTemplate
             * */
            if (Page.IsPostBack && Page.IsCallback && _run2ndTime && !_running2ndTime)
            {
                // this is a 2nd call from CreateChildControls, that we want to skip
                DataList listEmptyFacets = new DataList();
                listEmptyFacets.ID = "Facets";
                _moreResultsThanFacets = true;
                return listEmptyFacets;
            }

            // validation
            if (query.IsEmpty())
            {
                Utility.HandleException(null, true, Resource.ERROR_QUERY_EMPTY, Controls);
                return null;
            }
            string hiddenConstraints = (string.IsNullOrEmpty(_scope)) ? null : string.Format("scope:\"{0}\"", _scope.Trim('"'));
            SearchProcessor searchProcessor = new SearchProcessor(_cachingEnabled, this.Page, FacetColumns, query, _prefixEnabled, _moreResultsThanFacets, _run2ndTime, _running2ndTime, hiddenConstraints);
            DataTable dt = searchProcessor.GetCachedData(_selectColumns, _resultsPerPage, _longCacheTimeout, _fastCacheTimeout, out _usingCachedData, out _run2ndTime);
            
            _moreResultsThanFacets = Convert.ToBoolean(dt.ExtendedProperties["MoreResultsThanFacets"]);                    
            _processedTotal = dt.Rows.Count;
            _processedAt = Convert.ToDateTime(dt.ExtendedProperties["Created"]); 
            if (_run2ndTime && _moreResultsThanFacets)
            {
                if (!csm.IsClientScriptBlockRegistered(this.GetType(), key + "_SetRefineFacets"))
                {
                    csm.RegisterClientScriptBlock(this.GetType(), key + "_SetRefineFacets", "var refineFacets = true;", true);
                }
            }
            BuildFacets(dt);
            ApplyExclusions();
            ApplyRelationships();
            ApplyMappings();

            DataList listFacets = new DataList();
            listFacets.ID = "Facets";
            listFacets.ItemDataBound += new DataListItemEventHandler(listFacets_ItemDataBound);
            listFacets.CssClass = _listCss;
            FacetMenu facetMenu = _facets;
            // adding sorting
            string orderBy = GetColumnsAttribute("OrderBy");
            if (!string.IsNullOrEmpty(orderBy))
            {
                try
                {
                    FacetSortEnum facetsOrderBy = FacetSortEnum.None;
                    EnumConverter converter = (EnumConverter)TypeDescriptor.GetConverter(facetsOrderBy);
                    facetsOrderBy = (FacetSortEnum)converter.ConvertFromString(orderBy);
                    if (facetsOrderBy == FacetSortEnum.None)
                    {
                        facetMenu.Sort(FacetColumns);
                    }
                    else
                    {
                        facetMenu.SortOrder = facetsOrderBy;
                        facetMenu.Sort(facetMenu);
                    }
                }
                catch
                {
                    throw new NotSupportedException(Resource.ERROR_CONVERTING_ORDERBY);
                }
            }

            listFacets.DataSource = facetMenu;
            listFacets.DataBind();
            return listFacets;
        }

        private void ValidateColumns()
        {
            try
            {
                ServerContext cntxt = ServerContext.Current;
                SearchContext context = SearchContext.GetContext(cntxt);
                Schema schema = new Schema(context);
                ManagedPropertyCollection properties = schema.AllManagedProperties;
                //StringCollection facets = GetFacets();
                foreach (string facet in FacetColumns)
                {
                    if (!properties.Contains(facet))
                    {
                        // here might be a hidden managed property that doesn't show in API e.g. DETECTEDLANGUAGE
                        Utility.HandleException(null, false, Resource.ERROR_LOADING_FACETS, Controls); 
                    }
                }
            }
            catch (Exception ex)
            {
                Utility.HandleException(ex, true, Resource.ErrorMessageSearchFacets, Controls);
            }
        }


        #endregion

        #region ICallbackEventHandler Members

        public string GetCallbackResult()
        {
            return _facetHtml;
        }

        public void RaiseCallbackEvent(string eventArgument)
        {
            if (_run2ndTime && _moreResultsThanFacets)
            {
                if (!string.IsNullOrEmpty(eventArgument))
                {
                    SearchQuery q = new SearchQuery(eventArgument);
                    if (q.IsAdvancedSearch)
                    {
                        query = q;
                        if (Page.IsCallback && Page.IsPostBack)
                        {
                            // assume advanced search hit for the 2nd run.
                            // need to refill query
                            query.SyncFormData();
                        }
                    }
                }
                _running2ndTime = true;
                SecondRun();
            }
        }

        private void SecondRun()
        {
            DataList listFacets = CreateFacetsList();
            if (listFacets != null)
            {
                Controls.Add(listFacets);
                _facetHtml = Utility.GetHtmlFromControl(listFacets);
                Controls.Remove(listFacets);
                _facetHtml = string.Concat(_facetHtml, ExtraArgs());
            }
        }

        private string ExtraArgs()
        {
            StringBuilder builder = new StringBuilder();
            builder.Append(Constants.HTML_DELIMITER);
            builder.Append(_processedTotal);
            builder.Append(Constants.HTML_DELIMITER);
            builder.Append(_usingCachedData.ToString().ToLower());
            builder.Append(Constants.HTML_DELIMITER);
            builder.Append(_processedAt.ToLocalTime());
            return builder.ToString();
        }
        #endregion

        #region Connections
        [ConnectionProvider("Facet Settings", "FacetSettings", AllowsMultipleConnections = true)]
        public XmlNode FacetSettings()
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(_selectColumns);
            return doc.DocumentElement;
        }
        #endregion

    }

}
